LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

SpringAI-Alibaba

2025/7/8 AI

Spring AI Ailibaba简介

SpringAlAlibaba是基于SpringAI构建的框架,专注于与阿里云生态的深度集成。适合国内开发者,尤其是需要快速接入阿里云AI能力的场景。

LangChain4j是 LangChain项目的Java移植版本,专注于构建多模态、多参与者的AI应用,适合需要构建复杂AI工作流和多模态应用的开发者

文档:Spring AI Alibaba实战| ProcessOn免费在线作图,在线流程图,在线思维导图

官方文档:Spring AI Alibaba 官网_快速构建 JAVA AI 应用

项目:ai-demo: 本项目围绕 Spring AI Alibaba 技术栈打造实战课程《Spring AI Alibaba 从入门到进阶实战》,由 Fox 精心开发。课程从基础入门到深度实战,覆盖 Spring AI Alibaba 核心原理、大模型(含本地与云端,如 Ollama、通义千问 )集成、多模态功能(文生图、语音交互 )、RAG 架构、Function Calling、MCP 协议等关键技术,更通过电商智

[Spring AI Alibaba 官网_快速构建 JAVA AI 应用] (http://java2ai.com/)

核心概念

  • 模型(Model)
  • 提示(Prompt)
  • 嵌入(Embedding)
  • Token
  • 结构化输出(Structured Output)
  • 微调(Fine Tuning)
  • 检索增强生成(RAG)
  • 函数调用(Function Calling)
  • 评估人工智能的回答(Evaluation)
同步接口实现
  @GetMapping("/chat")
    public String chat(@RequestParam String input) {
        return chatClient.prompt().user(input).call().content();
    }
流式响应实现
   @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> stream(String input) {
        return this.chatClient.prompt()
                .user(input)
                .stream()
                .content();
    }

ChatClient

作用:通过 ChatClient,开发者可以更专注于业务逻辑而非底层协调,显著提升基于 LLM 的应用开发效率。
适用场景:快速开发:适合需要快速构建端到端 AI 交互的应用(如聊天机器人、问答系统)。复杂流程封装:当需要协调多个组件(如 LLM + 记忆 + 工具)时,减少样板代码。

返回实体类 .entity
    @GetMapping("/movies")
    public ActorFilms movies(@RequestParam(value = "input") String input) throws Exception {
        return this.chatClient.prompt()
                .user(input)
                .call()
                .entity(ActorFilms.class);
    }

curl --location --request GET 'http://localhost:1000/movies' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json'
指定消息角色 .defaultSystem
    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultSystem("你是一个演员,请列出你所参演的电影")
                .build();
    }
    @GetMapping("/chat")
    public String chat(@RequestParam(value = "input") String input) {

        return this.chatClient.prompt()
                .user(input)
                .call()
                .content();
    }


curl --location --request GET 'http://localhost:1000/chat' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json'
package com.fox.alibabaaidemo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public  class AIController {
      private final ChatClient chatClient;
    public AIController(ChatClient.Builder builder) {
        this.chatClient =  builder
                .defaultSystem("你是一个友好的聊天机器人,回答问题时要使用{voice}的语气")
                .build();
    }
      @GetMapping("/ai")
      Map<String, String> completion(@RequestParam(value = "message", defaultValue = "说一个笑话") String message, String voice) {
        return Map.of(
            "completion",
            this.chatClient.prompt()
                .system(sp -> sp.param("voice", voice))
                .user(message)
                .call()
                .content());
      }
    }

GET:http://localhost:1000/ai
voice  沈腾

其他默认设置

除了 defaultSystem 之外,您还可以在 ChatClient.Builder 级别上指定其他默认提示。

  • defaultOptions(ChatOptions chatOptions):传入 ChatOptions 类中定义的可移植选项或特定于模型实现的如 DashScopeChatOptions 选项。有关特定于模型的ChatOptions实现的更多信息,请参阅 JavaDocs。
  • defaultFunction(String name, String description, java.util.function.Function<I, O> function)name 用于在用户文本中引用该函数,description解释该函数的用途并帮助 AI 模型选择正确的函数以获得准确的响应,参数 function 是模型将在必要时执行的 Java 函数实例。
  • defaultFunctions(String... functionNames):应用程序上下文中定义的 java.util.Function 的 bean 名称。
  • defaultUser(String text)defaultUser(Resource text)defaultUser(Consumer<UserSpec> userSpecConsumer) 这些方法允许您定义用户消息输入,Consumer<UserSpec>允许您使用 lambda 指定用户消息输入和任何默认参数。
  • defaultAdvisors(RequestResponseAdvisor... advisor):Advisors 允许修改用于创建 Prompt 的数据,QuestionAnswerAdvisor 实现通过在 Prompt 中附加与用户文本相关的上下文信息来实现 Retrieval Augmented Generation 模式。
  • defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer):此方法允许您定义一个 Consumer 并使用 AdvisorSpec 配置多个 Advisor,Advisor 可以修改用于创建 Prompt 的最终数据,Consumer<AdvisorSpec> 允许您指定 lambda 来添加 Advisor 例如 QuestionAnswerAdvisor

您可以在运行时使用 ChatClient 提供的不带 default 前缀的相应方法覆盖这些默认值。

  • options(ChatOptions chatOptions)

  • function(String name, String description, java.util.function.Function<I, O> function)

  • functions(String... functionNames)

  • user(String text)user(Resource text)user(Consumer<UserSpec> userSpecConsumer)

  • advisors(RequestResponseAdvisor... advisor)

  • advisors(Consumer<AdvisorSpec> advisorSpecConsumer)

【基于内存存储】多轮对话记忆管理对话记忆

对话模型(Chat Model)

对话模型(Chat Model)接收一系列消息(Message)作为输入,与模型 LLM 服务进行交互,并接收返回的聊天消息(Chat Message)作为输出。相比于普通的程序输入,模型的输入与输出消息(Message)不止支持纯字符文本,还支持包括语音、图片、视频等作为输入输出。同时,在 Spring AI Alibaba 中,消息中还支持包含不同的角色,帮助底层模型区分来自模型、用户和系统指令等的不同消息。

Spring AI Alibaba 复用了 Spring AI 抽象的 Model API,并与通义系列大模型服务进行适配(如通义千问、通义万相等),目前支持纯文本聊天、文生图、文生语音、语音转文本等。以下是框架定义的几个核心 API:

  • ChatModel,文本聊天交互模型,支持纯文本格式作为输入,并将模型的输出以格式化文本形式返回。
  • ImageModel,接收用户文本输入,并将模型生成的图片作为输出返回。
  • AudioModel,接收用户文本输入,并将模型合成的语音作为输出返回。

Spring AI Alibaba 支持以上 Model 抽象与通义系列模型的适配,并通过 spring-ai-alibaba-starter AutoConfiguration 自动初始化了默认实例,因此我们可以在应用程序中直接注入 ChatModel、ImageModel 等 bean,当然在需要的时候也可以自定义 Model 实例。

对话记忆介绍

”大模型的对话记忆”这一概念,根植于人工智能与自然语言处理领域,特别是针对具有深度学习能力的大型语言模型而言,它指的是模型在与用户进行交互式对话过程中,能够追踪、理解并利用先前对话上下文的能力。 此机制使得大模型不仅能够响应即时的输入请求,还能基于之前的交流内容能够在对话中记住先前的对话内容,并根据这些信息进行后续的响应。这种记忆机制使得模型能够在对话中持续跟踪和理解用户的意图和上下文,从而实现更自然和连贯的对话。

我们在调用大模型的过程中,如果自己维护多轮的对话记忆,通常情况下调用代码如下

import java.util.ArrayList;

List<Message> messages = new ArrayList<>();

//第一轮对话
messages.add(new SystemMessage("你是一个旅游规划师"));
messages.add(new UserMessage("我想去新疆"));
ChatResponse response = chatModel.call(new Prompt(messages));
String content = response.getResult().getOutput().getContent();

messages.add(new AssistantMessage(content));

        //第二轮对话
messages.add(new UserMessage("能帮我推荐一些旅游景点吗?"));
response = chatModel.call(new Prompt(messages));
content = response.getResult().getOutput().getContent();

messages.add(new AssistantMessage(content));

        //第三轮对话
messages.add(new UserMessage("那里这两天的天气如何?"));
response = chatModel.call(new Prompt(messages));
content = response.getResult().getOutput().getContent();

System.out.printf("content: %s\n", content);

基于memory的对话记忆advisors是增强器

如何让大模型有对话记忆呢?
Advisor(MessageChatMemoryAdvisor)、Memory(InMemoryChatMemory,RedisChatMemory)
使用ChatMemoryAdvisor与RedisChatMemory存储历史对话

ChatClient.builder(chatModel)
  .defaultAdvisors(new MessageChatMemoryAdvisor(redisChatMemory))
  .build();

spring-ai-alibaba支持基于chat memory的对话记忆,也就是不需要调用显示的记录每一轮的对话历史。下边是一个基于内存存储的对话记忆实现:

@RestController
@RequestMapping("/chat-memory")
public class ChatMemoryController {

    private final ChatClient chatClient;

    public ChatMemoryController(ChatModel chatModel) {
// 构建对话模型 基于内存的方式InMemory...
        this.chatClient = ChatClient
                .builder(chatModel)
                .defaultSystem("你是一个旅游规划师,请根据用户的需求提供旅游规划建议。")
                .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
//                .defaultAdvisors(new MessageChatMemoryAdvisor(new RedisChatMemory(
//                        "127.0.0.1",
//                        6379,
//                        null
//                )))
                .build();
    }

    /**
     * 获取内存中的聊天内容
     * 根据提供的prompt和chatId,从内存中获取相关的聊天内容,并设置响应的字符编码为UTF-8。
     *
     * @param prompt 用于获取聊天内容的提示信息
     * @param chatId 聊天的唯一标识符,用于区分不同的聊天会话
     * @param response HTTP响应对象,用于设置响应的字符编码
     * @return 返回包含聊天内容的Flux<String>对象
     */
    @GetMapping("/in-memory")
    public Flux<String> memory(
            @RequestParam("prompt") String prompt,
            @RequestParam("chatId") String chatId,
            HttpServletResponse response
    ) {

        response.setCharacterEncoding("UTF-8");
        return chatClient.prompt(prompt).advisors(
                a -> a
                        .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
        ).stream().content();
    }

GET:http://localhost:1000/chat-memory/in-memory
prompt  我想去新疆玩
chatId  Pluminary

One【我想去新疆玩】

太棒了!新疆是中国最美丽、最多元化的旅游目的地之一,拥有壮丽的自然风光、丰富的民族文化和悠久的历史遗迹。根据你的兴趣和时间安排,我可以为你量身定制一份详细的旅行计划。

以下是一份 7天经典新疆旅游路线推荐,适合第一次来新疆的游客:


🌄 7天经典新疆旅游线路(北疆线)

✅ 路线概览:

乌鲁木齐 → 天池 → 乌鲁木齐 → 奎屯 → 赛里木湖 → 果子沟 → 伊宁 → 那拉提草原 → 巴音布鲁克 → 独库公路 → 乌鲁木齐


📅 行程详情:

第1天:抵达乌鲁木齐

  • 抵达乌鲁木齐地窝堡国际机场或火车站。
  • 游览【国际大巴扎】,购买当地特产与手工艺品。
  • 晚上可品尝新疆特色美食:大盘鸡、烤包子、羊肉串等。

第2天:乌鲁木齐 → 天山天池 → 返回乌鲁木齐

  • 上午驱车前往【天山天池】,欣赏雪山、湖泊、松林交相辉映的美景。
  • 下午返回市区,参观【新疆博物馆】了解西域历史文化。
  • 可尝试维吾尔族风味晚餐。

第3天:乌鲁木齐 → 奎屯 → 赛里木湖

  • 乘车约6小时到达【赛里木湖】,被誉为“大西洋最后一滴眼泪”。
  • 环湖游览,拍摄湖光山色,入住湖边民宿或附近酒店。

第4天:赛里木湖 → 伊宁

  • 沿果子沟峡谷前行,穿越【果子沟大桥】,风景绝佳。
  • 到达伊宁市后,游览【喀赞其民俗区】,感受浓郁的维吾尔族风情。
  • 住伊宁。

第5天:伊宁 → 那拉提草原

  • 乘车约3小时到达【那拉提草原】,是新疆最美的高山草甸之一。
  • 骑马、徒步、摄影,体验草原牧民生活。
  • 住那拉提镇。

第6天:那拉提 → 巴音布鲁克 → 独库公路 → 乌鲁木齐

  • 驱车前往【巴音布鲁克草原】,探访著名的“九曲十八弯”开都河。
  • 沿【独库公路】北段返回乌鲁木齐,途中穿越天山山脉,风景震撼。
  • 全天行车时间较长(约8-10小时),建议提前准备干粮和水。

第7天:乌鲁木齐 → 返程

  • 根据航班/火车时间安排返程。
  • 可在市区再次购物或补拍未尽兴的景点。

💡 小贴士:

  • 最佳旅游季节:每年6月-10月是新疆的最佳旅游季节,气候宜人,风景最美。
  • 交通方式
    • 自驾游是探索新疆最好的方式,路况较好。
    • 若不自驾,也可以选择包车或参加当地品质团。
  • 注意事项
    • 新疆昼夜温差大,注意带好保暖衣物。
    • 注意防晒,紫外线较强。
    • 尊重少数民族风俗习惯,避免敏感话题。

🍽️ 必吃美食推荐:

  • 烤包子
  • 手抓饭
  • 羊肉串
  • 盘吉尔塔格麻饼
  • 牛骨头汤
  • 新疆酸奶+馕

如果你有更长的时间,还可以考虑延伸到南疆(喀什、塔克拉玛干沙漠、帕米尔高原)或者北疆深度游(可可托海、喀纳斯、禾木)。

你可以告诉我:

  • 出行时间
  • 同行人数
  • 是否自驾
  • 是否希望加入人文历史或摄影元素

我可以为你定制更详细的行程、住宿推荐和预算方案哦!

Tow [prompt:有哪些好玩的地方?]

当然可以!作为一个旅游规划师,我会根据不同的旅行兴趣、时间和预算,为你推荐新疆最值得一去的好玩地方。新疆地大景美,玩法多样,既有自然风光,也有浓郁的民族风情和历史人文。


🌄 新疆十大必玩景点推荐(按类型分类)

一、自然风光类

1. 喀纳斯湖景区

  • 关键词:神秘湖怪、变色湖、图瓦人村落
  • 推荐理由:被誉为“人间仙境”,四季皆美,秋季尤胜。
  • 适合人群:摄影爱好者、自然探险者

2. 赛里木湖

  • 关键词:大西洋最后一滴眼泪、花海、湖光山色
  • 推荐理由:夏季油菜花盛开,湖水清澈见底,是北疆最美湖泊之一。

3. 那拉提草原

  • 关键词:空中草原、骑马、牧民风情
  • 推荐理由:夏季草原如画,适合徒步、骑马、拍照。

4. 巴音布鲁克草原 & 九曲十八弯

  • 关键词:天鹅故乡、开都河九曲、日落绝美
  • 推荐理由:中国第二大草原,电影《飞驰人生》取景地。

5. 独库公路

  • 关键词:天山之路、一日四季、自驾天堂
  • 推荐理由:被誉为“中国最美景观大道”,连接南北疆。

二、历史文化类

6. 交河故城 & 高昌故城

  • 关键词:丝绸之路遗迹、千年古城
  • 推荐理由:感受古代西域文明的辉煌与沧桑。

7. 克孜尔千佛洞

  • 关键词:佛教艺术瑰宝、壁画、石窟群
  • 推荐理由:比敦煌莫高窟更早开凿,艺术价值极高。

8. 香妃墓(阿帕克霍加墓)

  • 关键词:爱情传说、伊斯兰建筑
  • 推荐理由:喀什最具代表性的古迹之一。

三、民俗风情类

9. 喀什老城(艾提尕尔清真寺)

  • 关键词:维吾尔族风情、夜市、手工艺
  • 推荐理由:南疆文化中心,充满异域风情。

10. 塔克拉玛干沙漠 / 塔中沙漠公路

  • 关键词:死亡之海、星空露营、越野挑战
  • 推荐理由:体验极限风光,感受荒漠中的壮美。

🏞️ 按线路推荐热门玩法

线路类型 推荐景点 特点
北疆经典线 天池、赛里木湖、那拉提、巴音布鲁克、独库公路 风景优美、适合首次来新疆
南疆深度线 喀什、塔克拉玛干沙漠、帕米尔高原、塔县、白沙湖 民俗浓郁、神秘原始
北疆深度线 喀纳斯、禾木、可可托海 秋季绝美、摄影圣地
自驾线路 独库公路、沙漠公路、喀纳斯环线 风景多变、自由灵活

📅 根据时间长短推荐玩法

时间安排 推荐行程
5-7天 北疆经典线:乌鲁木齐 → 赛里木湖 → 那拉提 → 巴音布鲁克 → 独库公路 → 乌鲁木齐
8-10天 北疆+南疆组合:乌鲁木齐 → 喀纳斯 → 那拉提 → 伊宁 → 喀什 → 帕米尔高原
10天以上 全疆深度游:涵盖北疆、南疆、东疆,体验多元文化与极致风光

🎯 小贴士:如何选择适合你的路线?

  1. 喜欢自然风光? → 喀纳斯、赛里木湖、那拉提
  2. 喜欢人文历史? → 交河故城、克孜尔千佛洞、喀什老城
  3. 喜欢冒险刺激? → 塔克拉玛干沙漠、独库公路自驾
  4. 喜欢摄影? → 喀纳斯秋景、巴音布鲁克日落、帕米尔高原星空
  5. 亲子出游? → 乌鲁木齐博物馆、天山天池、那拉提草原

如果你能告诉我以下信息,我可以帮你定制专属行程:

  • 出行时间(几月份)
  • 出行人数(是否带老人/小孩)
  • 是否自驾或包车
  • 偏好自然/人文/美食/摄影等哪一类
  • 预算范围(经济型/舒适型/豪华型)

期待你的回复,我将为你打造一份专属新疆旅行计划!✨

当然,开发者也可以自行实现ChatMemory基于类似于文件、Redis等方式进行上下文内容的存储和记录。

【基于Redis存储】多轮对话记忆管理对话记忆

这里Redis的官方依赖有问题 需要自己修改再去使用 不算得上是重构 要去重写建立新的
可以将下面的打成包 使用的时候只需要引入依赖即可

C:\Users\Pluminary\Desktop\HouDuan\ai-demo\my-redis-memory\src\main\java\com\fox\myredismemory\RedisChatMemory.java
package com.fox.myredismemory;

import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.UserMessage;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;

/**
 *
 * 基于Redis的聊天记忆实现。
 * 该类实现了ChatMemory接口,提供了将聊天消息存储到Redis中的功能。
 *
 * @author Fox
 */
public class RedisChatMemory implements ChatMemory, AutoCloseable {

    private static final Logger logger = LoggerFactory.getLogger(RedisChatMemory.class);

    private static final String DEFAULT_KEY_PREFIX = "chat:";

    private static final String DEFAULT_HOST = "127.0.0.1";

    private static final int DEFAULT_PORT = 6379;

    private static final String DEFAULT_PASSWORD = null;

    private final JedisPool jedisPool;


    private final ObjectMapper objectMapper;

    public RedisChatMemory() {

        this(DEFAULT_HOST, DEFAULT_PORT, DEFAULT_PASSWORD);
    }

    public RedisChatMemory(String host, int port, String password) {

        JedisPoolConfig poolConfig = new JedisPoolConfig();

        this.jedisPool = new JedisPool(poolConfig, host, port, 2000, password);
        this.objectMapper = new ObjectMapper();
        logger.info("Connected to Redis at {}:{}", host, port);
    }

    @Override
    public void add(String conversationId, List<Message> messages) {

        String key = DEFAULT_KEY_PREFIX + conversationId;

        AtomicLong timestamp = new AtomicLong(System.currentTimeMillis());

        try (Jedis jedis = jedisPool.getResource()) {
            // 使用pipeline批量操作提升性能
            var pipeline = jedis.pipelined();
            messages.forEach(message ->
                    pipeline.hset(key, String.valueOf(timestamp.getAndIncrement()), message.toString())
            );
            pipeline.sync();
        }

        logger.info("Added messages to conversationId: {}", conversationId);
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {

        String key = DEFAULT_KEY_PREFIX + conversationId;

        try (Jedis jedis = jedisPool.getResource()) {
            Map<String, String> allMessages = jedis.hgetAll(key);
            if (allMessages.isEmpty()) {
                return List.of();
            }

            return allMessages.entrySet().stream()
                    .sorted((e1, e2) ->
                            Long.compare(Long.parseLong(e2.getKey()), Long.parseLong(e1.getKey()))
                    )
                    .limit(lastN)
                    .map(entry -> new UserMessage(entry.getValue()))
                    .collect(Collectors.toList());
        }


    }

    @Override
    public void clear(String conversationId) {

        String key = DEFAULT_KEY_PREFIX + conversationId;

        try (Jedis jedis = jedisPool.getResource()) {
            jedis.del(key);
        }
        logger.info("Cleared messages for conversationId: {}", conversationId);
    }

    @Override
    public void close() {
        try (Jedis jedis = jedisPool.getResource()) {
            if (jedis != null) {

                jedis.close();

                logger.info("Redis connection closed.");
            }
            if (jedisPool != null) {

                jedisPool.close();

                logger.info("Jedis pool closed.");
            }
        }

    }

    public void clearOverLimit(String conversationId, int maxLimit, int deleteSize) {
        try {
            String key = DEFAULT_KEY_PREFIX + conversationId;
            try (Jedis jedis = jedisPool.getResource()) {
                List<String> all = jedis.lrange(key, 0, -1);

                if (all.size() >= maxLimit) {
                    all = all.stream().skip(Math.max(0, deleteSize)).toList();
                }
                this.clear(conversationId);
                for (String message : all) {
                    jedis.rpush(key, message);
                }
            }
        }
        catch (Exception e) {
            logger.error("Error clearing messages from Redis chat memory", e);
            throw new RuntimeException(e);
        }
    }

}
@RestController
@RequestMapping("/chat-memory")
public class ChatMemoryController {

    private final ChatClient chatClient;

    public ChatMemoryController(ChatModel chatModel) {
// 构建对话模型 基于内存的方式InMemory...
        this.chatClient = ChatClient
                .builder(chatModel)
                .defaultSystem("你是一个旅游规划师,请根据用户的需求提供旅游规划建议。")
//                .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
                .defaultAdvisors(new MessageChatMemoryAdvisor(new RedisChatMemory(
                        "127.0.0.1",
                        6379,
                        null
                )))
                .build();
    }

    
    /**
     * 从Redis中获取聊天内容
     * 根据提供的prompt和chatId,从Redis中检索聊天内容,并以Flux<String>的形式返回
     *
     * @param prompt 聊天内容的提示或查询关键字
     * @param chatId 聊天的唯一标识符,用于从Redis中检索特定的聊天内容
     * @param response HttpServletResponse对象,用于设置响应的字符编码为UTF-8
     * @return Flux<String> 包含聊天内容的反应式流
     */
    @GetMapping("/redis")
    public Flux<String> redis(
            @RequestParam("prompt") String prompt,
            @RequestParam("chatId") String chatId,
            HttpServletResponse response
    ) {

        response.setCharacterEncoding("UTF-8");

        return chatClient.prompt(prompt)
                .advisors(
                a -> a
                        .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)
                )
                .stream().content();
    }

GET:http://localhost:1000/chat-memory/redis
prompt  有什么好吃的?
chatId  Pluminary

打开Another Redis Desktop Manager即可看见KeyValue
这里也是基于上下文 第一个问题是我想去三亚 第二个问题有什么好吃的这里是基于第一个问题和回答去输出结果

Key:1752192461656
Value:UserMessage{content='我想去三亚', properties={messageType=USER}, messageType=USER}

ChatModel对话模型

ChatResponse里面是对话的json响应类

package com.fox.alibabaaidemo.controller;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.io.IOException;

@RestController
public class ChatModelController {
    private final ChatModel chatModel;

    public ChatModelController(@Qualifier("dashscopeChatModel") ChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @RequestMapping("/chat2")
    public String chat2(String input) {

        DashScopeChatOptions options = DashScopeChatOptions.builder()
                .withTemperature(0.9)
                .withMaxToken(1500)
           //     .withTopP(0.01)
                .build();

        Prompt prompt = new Prompt(input, options);
        ChatResponse response = chatModel.call(prompt);
        //ChatResponse response = chatModel.call(new Prompt(input));
        return response.getResult().getOutput().getText();
    }


    @RequestMapping("/streamChat")
    public Flux<String> streamChat(String input, HttpServletResponse response) throws IOException {
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("UTF-8");
        return chatModel.stream(input);
    }
}

GET:http://localhost:1000/chat2
input  我想去深圳,帮我做个规划

ImageModel实现文生图

package com.fox.alibabaaidemo.controller;

import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
import org.springframework.ai.image.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ImageModelController {
    private final ImageModel imageModel;

    ImageModelController(@Qualifier("dashScopeImageModel") ImageModel imageModel) {
        this.imageModel = imageModel;
    }

    @RequestMapping("/image")
    public String image(String input) {
        ImageOptions options = ImageOptionsBuilder.builder()
                .model("wanx2.1-t2i-turbo")
                .height(1024)
                .width(1024)
                .build();

        ImagePrompt imagePrompt = new ImagePrompt(input, options);
        ImageResponse response = imageModel.call(imagePrompt);
        String imageUrl = response.getResult().getOutput().getUrl();

        return "redirect:" + imageUrl;
    }
}

GET:http://localhost:1000/image
input  森林中的绝美精灵,手里拿着弓

AudioModel文生语音&语音转文本

文本生成语音
package com.fox.alibabaaidemo.controller;

import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionModel;
import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionOptions;
import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisModel;
import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisPrompt;
import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisResponse;
import org.springframework.ai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;

/**
 * @author: Fox
 * @Desc:
 **/
@RestController
@RequestMapping("/audio")
public class AudioModelController {

    private final SpeechSynthesisModel speechSynthesisModel;

    @Autowired
    public AudioModelController(SpeechSynthesisModel speechSynthesisModel) {
        this.speechSynthesisModel = speechSynthesisModel;
    }

    @GetMapping("/synthesize")
    public ResponseEntity<byte[]> synthesizeSpeech(@RequestParam String text) throws IOException {
        // 构建语音合成请求
        SpeechSynthesisPrompt prompt = new SpeechSynthesisPrompt(text);

        // 调用模型生成语音
        SpeechSynthesisResponse response = speechSynthesisModel.call(prompt);
        ByteBuffer audioData = response.getResult().getOutput().getAudio();

        // 将 ByteBuffer 转换为字节数组
        byte[] audioBytes = new byte[audioData.remaining()];
        audioData.get(audioBytes);

        // 返回音频流(MP3格式)
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header("Content-Disposition", "attachment; filename=output.mp3")
                .body(audioBytes);
    }
}

GET:http://localhost:1000/audio/synthesize
text  我真的太喜欢点赞啦
语音生成文本
package com.fox.alibabaaidemo.controller;

import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionModel;
import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionOptions;
import org.springframework.ai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.MalformedURLException;

/**
 * @author: Fox
 * @Desc:
 **/
@RestController
public class AudioModelController2 {

    private static final String AUDIO_RESOURCES_URL = "https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav";

    private final DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel; //modelname:sensevoice-v1,paraformer-realtime-v2,paraformer-v2

    AudioModelController2(DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel){
        this.dashScopeAudioTranscriptionModel = dashScopeAudioTranscriptionModel;
    }

    @GetMapping("/audio")
    public String audio() throws MalformedURLException {
        Resource resource =new UrlResource(AUDIO_RESOURCES_URL);

        AudioTranscriptionPrompt prompt = new AudioTranscriptionPrompt(resource,
                DashScopeAudioTranscriptionOptions.builder()
                        .withModel("sensevoice-v1")
                        .build());

        return dashScopeAudioTranscriptionModel.call(prompt).getResult().getOutput();
    }
}

GET:http://localhost:1000/audio

提示词 (Prompt).create

Prompt 是引导 AI 模型生成特定输出的输入格式,Prompt 的设计和措辞会显著影响模型的响应。

Prompt 最开始只是简单的字符串,随着时间的推移,prompt 逐渐开始包含特定的占位符,例如 AI 模型可以识别的 “USER:”、“SYSTEM:” 等。阿里云通义模型可通过将多个消息字符串分类为不同的角色,然后再由 AI 模型处理,为 prompt 引入了更多结构。每条消息都分配有特定的角色,这些角色对消息进行分类,明确 AI 模型提示的每个部分的上下文和目的。这种结构化方法增强了与 AI 沟通的细微差别和有效性,因为 prompt 的每个部分在交互中都扮演着独特且明确的角色。

Prompt 中的主要角色(Role)包括:

  • 系统角色(System Role):指导 AI 的行为和响应方式,设置 AI 如何解释和回复输入的参数或规则。这类似于在发起对话之前向 AI 提供说明。
  • 用户角色(User Role):代表用户的输入 - 他们向 AI 提出的问题、命令或陈述。这个角色至关重要,因为它构成了 AI 响应的基础。
  • 助手角色(Assistant Role):AI 对用户输入的响应。这不仅仅是一个答案或反应,它对于保持对话的流畅性至关重要。通过跟踪 AI 之前的响应(其“助手角色”消息),系统可确保连贯且上下文相关的交互。助手消息也可能包含功能工具调用请求信息。它就像 AI 中的一个特殊功能,在需要执行特定功能(例如计算、获取数据或不仅仅是说话)时使用。
  • 工具/功能角色(Tool/Function Role):工具/功能角色专注于响应工具调用助手消息返回附加信息。
API 概览
Prompt

通常使用 ChatModel 的 call() 方法,该方法接受 Prompt 实例并返回 ChatResponse。

Prompt 类充当有组织的一系列 Message 对象和请求 ChatOptions 的容器。每条消息在提示中都体现了独特的角色,其内容和意图各不相同。这些角色可以包含各种元素,从用户查询到 AI 生成的响应再到相关背景信息。这种安排可以实现与 AI 模型的复杂而详细的交互,因为提示是由多条消息构成的,每条消息都被分配了在对话中扮演的特定角色。

该类实现的接口支持提示创建的不同方面:

PromptTemplateStringActions 专注于创建和呈现提示字符串,代表提示生成的最基本形式。

PromptTemplateMessageActions 专门用于通过生成和操作 Message 对象来创建提示。

PromptTemplateActions 旨在返回 Prompt 对象,该对象可以传递给 ChatModel 以生成响应。

虽然这些接口可能在许多项目中没有得到广泛使用,但它们展示了创建提示的不同方法。

实现ConfigurablePromptTemplateFactory
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\java\com\fox\promptdemo\config\PromptTemplateConfig.java

package com.fox.promptdemo.config;

import com.alibaba.cloud.ai.prompt.ConfigurablePromptTemplateFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PromptTemplateConfig {

    @Bean
    public ConfigurablePromptTemplateFactory configurablePromptTemplateFactory() {
        // 这里假设ConfigurablePromptTemplateFactory有一个无参构造函数
        return new ConfigurablePromptTemplateFactory();
        // 如果需要配置参数,可以在这里进行配置
        // return new ConfigurablePromptTemplateFactory(param1, param2);
    }
}
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\java\com\fox\promptdemo\controller\PromptTemplateController.java
@GetMapping("/prompt-template")
    public AssistantMessage generate(
            @RequestParam(value = "author", defaultValue = "鲁迅") String author
    ) {

        ConfigurablePromptTemplate template = configurablePromptTemplateFactory.getTemplate("test-template");

        if (template == null) {
            template = configurablePromptTemplateFactory.create("test-template",
                    "请列出 {author} 最著名的三本书。");
        }

        Prompt prompt;
        if (StringUtils.hasText(author)) {
            prompt = template.create(Map.of("author", author));
        } else {
            prompt = template.create();
        }

        return chatClient.prompt(prompt)
                .call()
                .chatResponse()
                .getResult()
                .getOutput();
    }

GET:http://localhost:10007/example/ai/prompt-template
author  鲁迅
实现PromptTemplate
@RestController
@RequestMapping("/example/ai")
public class PromptTemplateController {

    private final ChatClient chatClient;

    private final ConfigurablePromptTemplateFactory configurablePromptTemplateFactory;
        
    // 这里的资源是以st文件类型放在了resources的prompts里面
    @Value("classpath:/prompts/joke-prompt.st")
    private Resource jokeResource;

    @GetMapping("/prompt")
    public AssistantMessage completion(
            @RequestParam(value = "adjective", defaultValue = "有趣") String adjective,
            @RequestParam(value = "topic", defaultValue = "奶牛") String topic
    ) {

        PromptTemplate promptTemplate = new PromptTemplate(jokeResource);
        Prompt prompt = promptTemplate.create(Map.of("adjective", adjective, "topic", topic));

        return chatClient.prompt(prompt)
                .call()
                .chatResponse()
                .getResult()
                .getOutput();
    }
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\resources\prompts\joke-prompt.st
给我讲一个关于 {topic} 的 {adjective} 笑话

GET:http://localhost:10007/example/ai/prompt
adjective  有趣的
topic  猫
实现SystemPromptTemplate
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.promptdemo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/example/ai")
public class RoleController {

    private final ChatClient chatClient;

    @Value("classpath:/prompts/system-message.st")
    private Resource systemResource;

    @Autowired
    public RoleController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }
// defaultValue是没有传入时候的默认值
    @GetMapping("/roles")
    public AssistantMessage generate(
            @RequestParam(value = "message",
            defaultValue = "请介绍一下海盗黄金时代的三位著名海盗,以及他们为什么这样做。为每个海盗至少写一句话。") String message,
            @RequestParam(value = "name", defaultValue = "Fox") String name,
            @RequestParam(value = "voice", defaultValue = "海盗") String voice
    ) {

        UserMessage userMessage = new UserMessage(message);

        SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
        // .createMessage创建一个系统消息
        Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));

        return chatClient.prompt(new Prompt(List.of(userMessage, systemMessage)))
                .call()
                .chatResponse()
                .getResult()
                .getOutput();
    }
}

GET:http://localhost:10007/example/ai/roles
name  Pluminary
voice  海盗
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\resources\prompts\system-message.st
你是一个有用的 AI 助手。
你是帮助人们查找信息的 AI 助手。
你的名字是 {name}
你应该使用你的姓名和 {voice} 的样式回复用户的请求。

静态RAG实现:提示词动态注入

当前实现的方式:

  • 属于 “文档预加载+条件触发” 模式
  • 通过 stuffit 参数实现 人工控制的上下文开关
  • 上下文直接来自预定义的静态资源文件
  • 本质上是一种轻量级的 静态RAG(Static RAG)

经典RAG:

  • 采用 “检索-排序-注入” 自动化流程
  • 依赖向量相似度计算动态选择上下文
  • 支持增量学习和实时知识更新
  • 典型流程:问题编码→向量检索→相关性过滤→上下文注入

适用场景建议
当前方式更适合:

  • 文档规模小(如产品说明书、FAQ等)
  • 需要严格控制的回答范围
  • 快速原型开发阶段
  • 对实时性要求不高的场景

需要升级到RAG当:

  • 文档超过100MB或频繁更新
  • 需要基于问题语义自动匹配上下文
  • 要求支持多文档源混合检索
  • 需要结合用户画像的个性化回答
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.promptdemo.controller;

import com.fox.promptdemo.entity.Completion;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/example/ai")
public class StuffController {

    private final ChatClient chatClient;

    @Value("classpath:/docs/bailian.md")
    private Resource docsToStuffResource;

    @Value("classpath:/prompts/qa-prompt.st")
    private Resource qaPromptResource;

    @Autowired
    public StuffController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping(value = "/stuff")
    public Completion completion(@RequestParam(value = "message", defaultValue = "给我推荐一款百炼系列的手机?")
                                     String message, @RequestParam(value = "stuffit", defaultValue = "false")
                                     boolean stuffit) {

        PromptTemplate promptTemplate = new PromptTemplate(qaPromptResource);

        Map<String, Object> map = new HashMap<>();
        map.put("question", message);
        if (stuffit) {
            map.put("context", docsToStuffResource);
        } else {
            map.put("context", "");
        }

        return new Completion(chatClient.prompt(promptTemplate.create(map)).call().content());
    }

}


GET:http://localhost:10007/example/ai/stuff
message  给我推荐一款百炼系列的手机?
stuffit  true
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\resources\prompts\qa-prompt.st
使用以下上下文来回答最后的问题。
如果你不知道答案,就说你不知道,不要试图编造答案。

{context}

问题: {question}
有用的答案:
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\resources\docs\bailian.md
# **百炼手机产品介绍**

欢迎来到未来科技的前沿,探索我们精心打造的智能手机系列,每一款都是为了满足您对科技生活的无限遐想而生。

**百炼X1** —— 畅享极致视界:搭载6.7英寸1440 x 3200像素超清屏幕,搭配120Hz刷新率,流畅视觉体验跃然眼前。256GB海量存储空间与12GB RAM强强联合,无论是大型游戏还是多任务处理,都能轻松应对。5000mAh电池长续航,加上超感光四摄系统,记录生活每一刻精彩。参考售价:4599 - 4999

**通义Vivid 7** —— 智能摄影新体验:拥有6.5英寸1080 x 2400像素全面屏,AI智能摄影功能让每一张照片都能展现专业级色彩与细节。8GB RAM与128GB存储空间确保流畅操作,4500mAh电池满足日常所需。侧面指纹解锁,便捷又安全。参考售价:2999 - 3299

**星尘S9 Pro** —— 创新视觉盛宴:突破性6.9英寸1440 x 3088像素屏下摄像头设计,带来无界视觉享受。512GB存储与16GB RAM的顶级配置,配合6000mAh电池与100W快充技术,让性能与续航并驾齐驱,引领科技潮流。参考售价:5999 - 6499。

**百炼Ace Ultra** —— 游戏玩家之选:配备6.67英寸1080 x 2400像素屏幕,内置10GB RAM与256GB存储,确保游戏运行丝滑无阻。5500mAh电池搭配液冷散热系统,长时间游戏也能保持冷静。高动态双扬声器,沉浸式音效升级游戏体验。参考售价:3999 - 4299。

**百炼Zephyr Z9** —— 轻薄便携的艺术:轻巧的6.4英寸1080 x 2340像素设计,搭配128GB存储与6GB RAM,日常使用游刃有余。4000mAh电池确保一天无忧,30倍数字变焦镜头捕捉远处细节,轻薄而不失强大。参考售价:2499 - 2799。

**百炼Flex Fold+** —— 折叠屏新纪元:集创新与奢华于一身,主屏7.6英寸1800 x 2400像素与外屏4.7英寸1080 x 2400像素,多角度自由悬停设计,满足不同场景需求。512GB存储、12GB RAM,加之4700mAh电池与UTG超薄柔性玻璃,开启折叠屏时代新篇章。此外,这款手机还支持双卡双待、卫星通话,帮助您在世界各地都能畅联通话。参考零售价:9999 - 10999。

每一款手机都是匠心独运,只为成就您手中的科技艺术品。选择属于您的智能伙伴,开启未来科技生活的新篇章。

实现结构化输出转JSON或Java对象(.entity)【文本转特定格式】

ChatClient

调用entity()方法

// 实体对象
ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("Generate the filmography of 5 movies for {actor}.")
                    .param("actor", "Tom Hanks"))
        .call()
        .entity(ActorsFilms.class);
List
List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
        .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
        .call()
        .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});


//使用 ListOutputConverter 将模型响应转换为 List:
List<String> flavors = ChatClient.create(chatModel).prompt()
                .user(u -> u.text("List five {subject}")
                            .param("subject", "ice cream flavors"))
                .call()
                .entity(new ListOutputConverter(new DefaultConversionService()));
Map< String, Object>
Map<String, Object> result = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("Provide me a List of {subject}")
                    .param("subject", "an array of numbers from 1 to 9 under their key name 'numbers'"))
        .call()
        .entity(new ParameterizedTypeReference<Map<String, Object>>() {});

com/fox/structureddemo/stream/StreamToBeanEntity.java
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.structureddemo.stream;

public class StreamToBeanEntity {

    private String title;
    private String author;
    private String date;
    private String content;

    public StreamToBeanEntity() {
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "StreamToBeanEntity{" +
                "title='" + title + '\'' +
                ", author='" + author + '\'' +
                ", date='" + date + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}
com/fox/structureddemo/stream/StreamToBeanController.java
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.structureddemo.stream;

import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.Objects;


@RestController
@RequestMapping("/example/stream")
public class StreamToBeanController {

    private final ChatClient chatClient;

    private static final Logger log = LoggerFactory.getLogger(StreamToBeanController.class);

    public StreamToBeanController(ChatClient.Builder builder) {
        // 使用builder对象构建ChatClient实例
        this.chatClient = builder.build();
    }


    /**
     * @return {@link com.fox.structureddemo.stream.StreamToBeanEntity}
     */
    @GetMapping("/play")
    public StreamToBeanEntity simpleChat(HttpServletResponse response) {

        response.setCharacterEncoding("UTF-8");

        var converter = new BeanOutputConverter<>(
                new ParameterizedTypeReference<StreamToBeanEntity>() { }
        );

        Flux<String> flux = this.chatClient.prompt()
                .user(u -> u.text("""
                        requirement: 请用大概 120 字,作者为 Fox ,为计算机的发展历史写一首现代诗;
                        format: 以纯文本输出 json,请不要包含任何多余的文字——包括 markdown 格式;
                        outputExample: {
                             "title": {title},
                             "author": {author},
                             "date": {date},
                             "content": {content}
                        };
                        """))
                .stream()
                .content();

        String result = String.join("\n", Objects.requireNonNull(flux.collectList().block()))
                .replaceAll("\\n", "")
                .replaceAll("\\s+", " ")
                .replaceAll("\"\\s*:", "\":")
                .replaceAll(":\\s*\"", ":\"");

        log.info("LLMs 响应的 json 数据为:{}", result);

        return converter.convert(result);
    }
}
com/fox/structureddemo/stream/StreamToJsonController.java
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.structureddemo.stream;

import com.alibaba.cloud.ai.dashscope.api.DashScopeResponseFormat;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/example/stream/json")
public class StreamToJsonController {

    private static final String DEFAULT_PROMPT = "你好,请以JSON格式介绍你自己!";

    private final ChatClient dashScopeChatClient;

    public StreamToJsonController(ChatModel chatModel) {

        DashScopeResponseFormat responseFormat = new DashScopeResponseFormat();
        responseFormat.setType(DashScopeResponseFormat.Type.JSON_OBJECT);

        this.dashScopeChatClient = ChatClient.builder(chatModel)
                .defaultOptions(
                        DashScopeChatOptions.builder()
                                .withTopP(0.7)
                                .withResponseFormat(responseFormat)
                                .build()
                )
                .build();
    }

    /**
     * @return {@link String}
     */
    @GetMapping("/play")
    public String simpleChat(HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        return dashScopeChatClient.prompt(DEFAULT_PROMPT)
                .call()
                .content();
    }

}

通义千问大模型调优

  • temperature:温度值越高,模型生成的内容就越丰富多样

    温度只是来调整候选Token集合的概率分布

    低值(0.2):适用于标准化回答(如退货政策查询)
    高值(0.8):适用于创意场景(如促销文案生成)低温度、中温度和高温度是依据通义千问plus模型的取值范围[0, 2)来划分。

    • 低温度(temperature=0.1):大模型选择“RAG”的概率远高于其它候选Token,这会导致其输出相对单一。
    • 中温度(temperature=0.7):候选Token之间概率分布相对平缓,除“RAG”之外的候选Token出现的概率都在增加。
    • 高温度(temperature=1.2):原低概率的候选Token出现概率进一步提升,打乱原有的排序,进一步提高了大模型输出的随机性。在上图中,“画画”出现的概率反而最高。
  • top_p:top_p值越高,大模型的输出结果随机性越高

    top_p控制候选Token集合的采样范围。

    设置top_p=0.9平衡多样性与准确性,防止低概率token干扰

为了确保生成内容的可控性,建议不要同时调整top_p和temperature。同时调整可能导致输出结果不可预测和复杂。你可以优先调整其中一种参数,观察其对结果的影响,再逐步微调。

  • top_k:top_k越大,生成内容越多样化;top_k越小,内容则更固

在通义千问系列模型中,参数topk也有类似topp的能力,可查阅通义千问API文档。它是一种采样机制,从概率排名前k的Token中随机选择一个进行输出。一般来说,topk越大,生成内容越多样化;topk越小,内容则更固定。当top_k设置为1时,模型仅选择概率最高的Token,输出会更加稳定,但也会导致缺乏变化和创意。

通义千问API参考_大模型服务平台百炼(Model Studio)-阿里云帮助中心

多个AI大模型无缝切换【👆图在上面👆】

如何实现多个AI大模型的无缝切换,比如DeepSeek-R1 调用失败可以请求降级调用 QwQ-32B。 AI网关方案实战

期,通义千问发布了一款全新的推理模型 QwQ-32B。在各类基准测试中,这个拥有 320 亿参数的模型展现出了与 DeepSeek-R1(6710 亿参数)相当的性能。这意味着:

  • 对于个人用户而言,QwQ-32B 能够直接在本地运行,且对设备的要求更低,适合在更小的移动设备上使用。
  • 对于企业用户来说,调用推理大模型 API 的成本可以进一步降低,最高可减少 90% 的费用。

在实际应用中,无缝切换多个大模型的需求日益凸显。例如,企业可能需要同时对接多个大模型,以满足不同业务场景的需求;当单个模型出现稳定性问题时,能够迅速回退到另一个模型,确保业务的连续性和稳定性;在 Multi Agent 场景下,一个复杂任务可能需要调用多个模型来协同完成。Higress AI 网关提供了一个强大的解决方案,支持多模型服务,并具备消费者鉴权、模型自动切换等高级功能。

本文将为您提供一份详细的教程,指导您如何使用 Higress AI 网关在 DeepSeek-R1 和 QwQ-32B 大模型之间实现无缝切换。

关于Higress网关的使用,可以参考我上一篇文章:阿里巴巴开源的云原生网关Higress实战

若想要Higress桌面版 先下载Docker Desktop再下载 Cygwin验证
Cygwin 安装是否成功

cygcheck -c cygwin

Pluminary@Pluminary ~
$ cygcheck -c cygwin
Cygwin Package Information
Package Version Status
cygwin 3.6.1-1 OK

cygwin下载及安装详细教程,windows使用linux的shell命令编译源码(win10) - 知乎
Windows 下 Higress 部署实践 | Higress

单机部署nacos 进入D:\nacos-docker-master\cmd
单机部署指令:docker-compose -f example/standalone-derby.yaml up
这里可以修改端口映射:D:\nacos-docker-master\example\standalone-derby.yaml 如下👇
正确的监控检查命令:curl http://192.168.31.103:8848/nacos/v3/console/health/readiness

version: "2"
services:
  nacos:
    image: nacos/nacos-server:${NACOS_VERSION}
    container_name: nacos-standalone
    environment:
      - PREFER_HOST_MODE=hostname
      - MODE=standalone
      - NACOS_AUTH_IDENTITY_KEY=serverIdentity
      - NACOS_AUTH_IDENTITY_VALUE=security
      - NACOS_AUTH_TOKEN=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg=
    volumes:
      - ./standalone-logs/:/home/nacos/logs
    ports:
      - "8848:8848"
      - "18080:8080"       
  prometheus:
    container_name: prometheus
    image: prom/prometheus:latest
    volumes:
      - ./prometheus/prometheus-standalone.yaml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    depends_on:
      - nacos
    restart: on-failure
  grafana:
    container_name: grafana
    image: grafana/grafana:latest
    ports:
      - 3000:3000
    restart: on-failure

随后在桌面打开cmd输入ipconfig寻找ip地址 记录下来
在桌面右键 → 选择 Git Bash Here,进入 Bash 环境
在 Git Bash 里执行命令:【ip地址是通过ipconfig获取】**这里要记得关闭梯子和代理 因为从阿里云拉镜像**

curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -c nacos://192.168.31.103:8848

★★

// 安装higress 进入bash
$ curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -c nacos://192.168.43.85:8848 --nacos-username=nacos --nacos-password=nacos --nacos-ns=higress-system --nacos-ns=higress-system

       // 这里是启动的nacos
cd D:\nacos-docker-master\example
// 重启docker(Nacos)
docker-compose -f standalone-derby.yaml down
docker-compose -f standalone-derby.yaml up -d

// 一键重启
docker restart nacos-standalone

    
       // 这里是启动higress    
// 在桌面启一个bash  启动higress
$ /c/Users/Pluminary/Desktop/higress/bin/startup.sh
// 如果改了配置 在bash里  【一定要先启动docker 才可以干别的】
cd /c/Users/Pluminary/Desktop/higress
./bin/shutdown.sh
./bin/startup.sh
    

然后就一直无敌报错:

http://192.168.31.103:8848/nacos/v1/console/health/readiness returns 000
Waiting for Nacos to get ready…
Waiting for Nacos to get ready…
Waiting for Nacos to get ready…
Waiting for Nacos to get ready…
Waiting for Nacos to get ready…

修改端口不占用冲突的8080即可

cd D:\nacos-docker-master\example

重启docker(Nacos)
docker-compose -f standalone-derby.yaml down
docker-compose -f standalone-derby.yaml up -d

一键重启

docker restart nacos-standalone




下一个报错:

Nacos is ready.
Initializing Nacos server…

Nacos 3.x isn’t fully supported yet.

If you do want to use Nacos 3.x, please add the following property into its application.properties file:
nacos.core.api.compatibility.console.enabled=true

Higress configuration failed with 255.
Failed to install Higress
For support, go to https://github.com/alibaba/higress.

这说明什么?
  • Higress 当前版本对 Nacos 3.x 支持不完善,需要兼容模式开启
  • 你的 Nacos 3.x 服务默认没开启兼容控制台API,导致健康检查接口返回异常或空

1. 修改 Nacos 配置,启用兼容模式

找到你的 Nacos 容器中 application.properties 文件,添加下面这一行:

nacos.core.api.compatibility.console.enabled=true

2. 重启 Nacos 容器

cd D:\nacos-docker-master\example
docker restart nacos-standalone

3. 访问健康接口测试

curl http://192.168.43.85:8848/nacos/v1/console/health/readiness

搞什么乱七八糟的配置 直接降级nacos 因为nacos3.xxx有很多不兼容的地方
D:\nacos-docker-master\exampleNACOS_VERSION=v2.3.1

Pluminary@Pluminary MINGW64 ~/Desktop
$ curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -c nacos://192.168.43.85:8848 --nacos-username=nacos --nacos-password=nacos --nacos-ns=higress-system 

Downloading https://github.com/higress-group/higress-
standalone/archive/refs/tags/v2.1.5.tar.gz...
==== Build Configurations ====
time="2025-07-12T00:40:46+08:00" level=warning msg="C:\\Users\\Pluminary\\Desktop\\higress\\compose\\docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion"
Nacos is ready.
Initializing Nacos server...
Use Nacos API v1
  Namespace higress-system already exists in Nacos.
  Fixed data encryption key is used. Skip config overwriting check.
Initializing API server configurations...
  Data encryption key already exists.
Initializing controller configurations...

 ___  ___  ___  ________  ________  _______   ________   ________
|\  \|\  \|\  \|\   ____\|\   __  \|\  ___ \ |\   ____\ |\   ____\
\ \  \\\  \ \  \ \  \___|\ \  \|\  \ \   __/|\ \  \___|_\ \  \___|_
 \ \   __  \ \  \ \  \  __\ \   _  _\ \  \_|/_\ \_____  \\ \_____  \
  \ \  \ \  \ \  \ \  \|\  \ \  \\  \\ \  \_|\ \|____|\  \\|____|\  \
   \ \__\ \__\ \__\ \_______\ \__\\ _\\ \_______\____\_\  \ ____\_\  \
    \|__|\|__|\|__|\|_______|\|__|\|__|\|_______|\_________\\_________\
                                                \|_________\|_________|

Higress is configured successfully.

Important Notes:
  Sensitive configurations are encrypted when saving to Nacos.
  When configuring another server with the same Nacos configuration service, please make sure to add the following argument so all servers use the same encryption key:
   

Usage:
  Start: /c/Users/Pluminary/Desktop/higress/bin/startup.sh
  Stop: /c/Users/Pluminary/Desktop/higress/bin/shutdown.sh
  View Component Statuses: /c/Users/Pluminary/Desktop/higress/bin/status.sh
  View Logs: /c/Users/Pluminary/Desktop/higress/bin/logs.sh
  Re-configure: /c/Users/Pluminary/Desktop/higress/bin/configure.sh -r

Happy Higressing!

访问Nacos:Nacos 或 地址改成localhost

✅ 配置项 1:secrets.nacos-auth-default

位置:

  • 命名空间: fc629691-b48f-4fa2-baa7-770dbaf8b63e(对应 higress-system
  • Data ID: secrets.nacos-auth-default
  • Group: DEFAULT_GROUP
  • 配置格式: YAML

配置内容:

username: nacos
password: nacos

✅ 配置项 2:configmaps.higress-config

位置:

  • 命名空间: fc629691-b48f-4fa2-baa7-770dbaf8b63e
  • Data ID: configmaps.higress-config
  • Group: DEFAULT_GROUP
  • 配置格式: YAML

配置内容:

apiVersion: v1
kind: ConfigMap
metadata:
  name: higress-config
  namespace: higress-system
data:
  ingressClass: higress

✅ 配置项 3:mcpbridges.default

位置:

  • 命名空间: fc629691-b48f-4fa2-baa7-770dbaf8b63e
  • Data ID: mcpbridges.default
  • Group: DEFAULT_GROUP
  • 配置格式: YAML

配置内容:

apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: default
  namespace: higress-system
spec:
  registries:
    - domain: httpbin.org
      name: httpbin
      port: 80
      type: dns
./bin/configure.sh -a -c nacos://192.168.31.103:8848 \
  --nacos-username=nacos \
  --nacos-password=nacos \
  --nacos-ns=higress-system

✅ 解决方案:使用 -r 参数强制重新配置

你需要强制重新执行配置流程并显式指定 nacos 命名空间,命令如下:

bash复制编辑./bin/configure.sh -a -r \
  -c nacos://192.168.31.103:8848 \
  --nacos-username=nacos \
  --nacos-password=nacos \
  --nacos-ns=higress-system \
  -k 7bbac1c2ab91ff074cb8d219c307d203

📌 说明:

  • -r:重新运行配置流程(一定要加)
  • --nacos-ns=higress-system:这才是决定配置写入哪个命名空间的参数
  • .env 中的 NACOS_NS 虽然也有用,但以你命令参数为主

什么是RAG

RAG:接收到问题后,去知识库查资料,得到检索知识后进行回答
微调:提前学习知识,丰富自己的能力,接收到问题后进行回答

RAG → 问题 → (LLM+知识) → 回复
    
                   问题
                    ↓
微调 → LLM → 知识 → LLM(new)
               ↘    ↓
                   回复
RAG工作原理:
  • 建立索引阶段

    文件上传 → 文档加载 → 内容分割 → 文本向量化 → 索引存储

  • 检索与生成阶段

    检索:检索阶段会召回与问题最相关的文本段。通过embedding模型对问题进行文本向量化,并与向量数据库的段落进行语义相似度的比较,找出最相关的段落。
    生成:在检索到相关的文本段后,RAG应用会将问题与文本段通过提示词模板生成最终的提示词,由大模型生成回复,这个阶段更多是利用大模型的总结能力,而不是大模型本身具有的知识。

  • 总结

    用户提问 → → → → → → 提示词 → 大语言模型 → 输出答案
    ↘ → 内容检索 → ↑

com/fox/ragdemo/config/RagConfig.java
package com.fox.ragdemo.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class RagConfig {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你将作为一名机器人产品的专家,对于用户的使用需求作出解答")
                .build();
    }

    @Bean
    VectorStore vectorStore(EmbeddingModel embeddingModel) {
        SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(embeddingModel)
                .build();

        // 生成一个机器人产品说明书的文档
        List<Document> documents = List.of(
                new Document("产品说明书:产品名称:智能机器人\n" +
                        "产品描述:智能机器人是一个智能设备,能够自动完成各种任务。\n" +
                        "功能:\n" +
                        "1. 自动导航:机器人能够自动导航到指定位置。\n" +
                        "2. 自动抓取:机器人能够自动抓取物品。\n" +
                        "3. 自动放置:机器人能够自动放置物品。\n"));

        simpleVectorStore.add(documents);
        return simpleVectorStore;
    }
}
com/fox/ragdemo/controller/RagController.java
package com.fox.ragdemo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/ai")
public class RagController {

    @Autowired
    private ChatClient chatClient;

    @Autowired
    private VectorStore vectorStore;


    @GetMapping(value = "/chat", produces = "text/plain; charset=UTF-8")
    public String generation(String userInput) {
        // 发起聊天请求并处理响应
        return chatClient.prompt()
                .user(userInput)
                // 调用知识库
                .advisors(new QuestionAnswerAdvisor(vectorStore))
                .call()
                .content();
    }
}
package com.fox.ragdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RagDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(RagDemoApplication.class, args);
    }
}

RAG+阿里云百炼

本地集成百炼智能体应用
本地RAG应用集成百炼知识库
知识库:
【学校简介】
阳光中学创建于1998年,是一所全日制寄宿制完全中学,坐落于江苏省苏州市。学校占地120亩,现有教职工120人,学生1500余人。多年来,阳光中学秉持“厚德载物,知行合一”的校训,致力于培养具有国际视野与中国灵魂的复合型人才。
________________________________________
【课程设置】
阳光中学设有初中和高中两个学段,开设课程包括语文、数学、英语、物理、化学、生物、历史、地理、政治、信息技术、音乐、美术和体育等。高一开始分文理科方向,同时提供选修课程如Python编程、人工智能基础、商务英语等,满足学生多元发展需求。
________________________________________
【师资力量】
学校现有特级教师3人,高级教师32人,硕士学历以上教师占比达70%。其中语文学科带头人王老师曾获全国语文优质课一等奖,数学组组长李老师拥有15年以上高三教学经验,多次带出省级高考状元。
________________________________________
【校园生活】
阳光中学注重学生全面发展,组织丰富的课外活动与社团,如机器人社、合唱团、辩论社、摄影社、篮球队等。每年5月举办校园艺术节,每年10月举行运动会,深受师生喜爱。校园内有图书馆、自助餐厅、医务室、心理咨询室等完善设施。
________________________________________
【校规制度】
1.    学生必须穿着校服上课,不得迟到、早退或旷课;
2.    晚自习时间为每周一至周五18:30—20:30;
3.    禁止携带手机等电子产品进课堂;
4.    严禁在校园内打架、吸烟、喝酒,一经发现严肃处理;
5.    每月评选“文明班级”“学习标兵”等荣誉称号。
________________________________________
【招生信息】
阳光中学每年春季开始招生,面向全国招收初一、高一新生。招生名额有限,需通过笔试和面试。学费标准为初中部每学期6000元,高中部每学期8000元,另设有奖学金与助学金政策,家庭困难学生可申请减免。
________________________________________
【常见问题解答】
Q:阳光中学是否提供住宿?
A:学校提供标准化学生宿舍,6人一间,配备空调、热水器与独立卫生间,设有生活老师管理。
Q:学校是否提供营养餐?
A:学生餐厅提供三餐,包括荤素搭配的营养套餐,每周公布菜单,确保健康卫生。
Q:如何申请插班?
A:插班生需在每学期开学前一个月提交申请,学校将根据学位情况及考试成绩决定是否录取。
Q:学校对艺术体育特长生有优惠吗?
A:是的。学校每年招收部分艺术、体育特长生,可享受降分录取及奖学金政策。
________________________________________
【校园热点资讯】
2025年6月,阳光中学高考再创佳绩,重本上线率达89%,其中7人被清华北大录取;
2025年3月,阳光中学学生在全国青少年人工智能竞赛中获得一等奖;
2024年10月,学校新建科学楼正式启用,建筑面积达4800平方米,配备多个功能实验室。
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\bailian-agent\src\main\java\com\fox\bailianagent\controller\BailianAgentRagController.java
package com.fox.bailianagent.controller;

import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgent;
import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgentOptions;
import com.alibaba.cloud.ai.dashscope.api.DashScopeAgentApi;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BailianAgentRagController {
  private DashScopeAgent agent;

  @Value("${spring.ai.dashscope.agent.app-id}")
  private String appId;

  public BailianAgentRagController(DashScopeAgentApi dashscopeAgentApi) {
    this.agent = new DashScopeAgent(dashscopeAgentApi);
  }

  @GetMapping("/bailian/agent/call")
  public String call(@RequestParam(value = "message") String message) {
    ChatResponse response = agent.call(
            new Prompt(message, DashScopeAgentOptions.builder()
                    // 与智能体的绑定
                    .withAppId(appId)
                    .build()));
    AssistantMessage app_output = response.getResult().getOutput();
    return app_output.getText();
  }
}

GET:http://localhost:8088/bailian/agent/call
message  学校简介是什么?
package com.fox.bailianagent;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BailianAgentApplication {

    public static void main(String[] args) {
        SpringApplication.run(BailianAgentApplication.class, args);
    }
}
server:
  port: 8088

spring:
  application:
    name: bailian-agent
  ai:
    dashscope:
      agent:
# https://bailian.console.aliyun.com/?tab=app#/app-center
        app-id: xxxxxxx
      api-key: xxxxxxx

SpringAIAlibaba本地集成百炼知识库实战通过代码将文件上传到百炼知识库

/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.fox.bailianragdemo.controller;

import com.fox.bailianragdemo.service.RagService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/ai")
public class CloudRagController {

    private final RagService cloudRagService;

    public CloudRagController(RagService cloudRagService) {
        this.cloudRagService = cloudRagService;
    }

    @GetMapping("/bailian/knowledge/importDocument")
    public void importDocument() {
        cloudRagService.importDocuments();
    }

    @GetMapping("/bailian/knowledge/generate")
    public Flux<String> generate(@RequestParam(value = "message",
            defaultValue = "你好,请问你的知识库文档主要是关于什么内容的?") String message) {
        return cloudRagService.retrieve(message).map(x -> x.getResult().getOutput().getText());
    }

}
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.fox.bailianragdemo.service;

import com.alibaba.cloud.ai.advisor.DocumentRetrievalAdvisor;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.rag.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.rag.retrieval.search.DocumentRetriever;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;


@Service()
public class CloudRagService implements RagService {

    private static final Logger logger = LoggerFactory.getLogger(CloudRagService.class);

    private static final String indexName = "阳光中学";

    @Value("classpath:/data/spring_ai_alibaba_quickstart.pdf")
    private Resource springAiResource;

    private static final String retrievalSystemTemplate = """
            上下文信息如下:
            ---------------------
            {question_answer_context}
            ---------------------
            根据上下文和提供的历史信息,而不是先验知识,回答用户问题。
            如果答案不在上下文中,请告知用户无法回答该问题。
            """;

    private final ChatClient chatClient;

    private final DashScopeApi dashscopeApi;

    public CloudRagService(ChatClient.Builder builder, DashScopeApi dashscopeApi) {
        DocumentRetriever retriever = new DashScopeDocumentRetriever(dashscopeApi,
                DashScopeDocumentRetrieverOptions.builder().withIndexName(indexName).build());

        this.dashscopeApi = dashscopeApi;
        this.chatClient = builder
                .defaultAdvisors(new DocumentRetrievalAdvisor(retriever, retrievalSystemTemplate))
                .build();
    }

    @Override
    public void importDocuments() {
        String path = saveToTempFile(springAiResource);

        // 1. import and split documents
        DocumentReader reader = new DashScopeDocumentCloudReader(path, dashscopeApi, null);
        List<Document> documentList = reader.get();
        logger.info("{} documents loaded and split", documentList.size());

        // 1. add documents to DashScope cloud storage 向量数据库
        VectorStore vectorStore = new DashScopeCloudStore(dashscopeApi, new DashScopeStoreOptions(indexName));
        vectorStore.add(documentList);
        logger.info("{} documents added to dashscope cloud vector store", documentList.size());
    }

    private String saveToTempFile(Resource springAiResource) {
        try {
            File tempFile = File.createTempFile("spring_ai_alibaba_quickstart", ".pdf");
            tempFile.deleteOnExit();

            try (InputStream inputStream = springAiResource.getInputStream();
                    FileOutputStream outputStream = new FileOutputStream(tempFile)) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            }

            return tempFile.getAbsolutePath();
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public Flux<ChatResponse> retrieve(String message) {
        return chatClient.prompt().user(message).stream().chatResponse();
    }

}
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.fox.bailianragdemo.service;

import org.springframework.ai.chat.model.ChatResponse;
import reactor.core.publisher.Flux;


public interface RagService {

    void importDocuments();

    Flux<ChatResponse> retrieve(String message);
}
server:
  port: 8088

spring:
  application:
    name: bailian-rag-demo
  ai:
    dashscope:
      api-key: xxxxx

工具Fuction Calling

允许语言大模型调用外部函数(Function)或API来执行特定任务或获取实时数据。
例如:
调用天气API查询实时天气。
调用数据库接口执行查询。 其核心是扩展模型的交互能力,使其能执行操作或获取结构化数据,而非仅依赖内部知识

应用场景

实时数据查询:如股票行情、天气、航班信息。
任务自动化:预订会议、下单、数据计算等。
系统集成:与CRM、ERP等业务系统交互(如创建客户记录)

和RAG的区别

RAG和Function Calling分别从知识增强和功能扩展两个维度提升LLM能力。选择取决于具体需求:

  • 需结合外部知识? → RAG
  • 需执行操作或获取实时数据? → Function Calling
  • 复杂场景:两者结合(如Agent框架中集成RAG和工具调用)
维度 RAG Function Calling
数据依赖 依赖外部知识库的覆盖和质量 依赖API或函数的可用性和稳定性
实时性 受限于知识库更新频率 可获取实时数据(如API返回结果)
输出形式 自然语言文本 结构化数据(Json)或操作结果
适用问题类型 需外部知识支持的复杂问题 需执行操作或获取动态数据的场景
实现复杂度 需构建高效检索系统(向量数据库) 需定义函数接口和参数规范

Function Calling获取天气信息

以Function为接口调用apply方法
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\toolcalling-demo\src\main\java\com\fox\toolcallingdemo\tool\weather\function\WeatherFunction.java

package com.fox.toolcallingdemo.tool.weather.function;

import java.util.function.Function;

public class WeatherFunction implements Function<WeatherFunction.WeatherRequest, String> {
    @Override
    public String apply(WeatherRequest request) {
        // 此处省略了实际的天气查询逻辑,直接返回一个示例字符串
        // 实际应用中需要根据请求参数调用天气API获取天气信息
        return "The weather in " + request.getCity() + " is sunny.";
    }
    public static class WeatherRequest {
        private String city;
        public String getCity() { return city; }
        public void setCity(String city) { this.city = city; }
    }
}
把函数以Bean形式交给Spring去管理
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\toolcalling-demo\src\main\java\com\fox\toolcallingdemo\tool\weather\function\FunctionConfig.java
    
package com.fox.toolcallingdemo.tool.weather.function;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

import java.util.function.Function;

@Configuration
public class FunctionConfig {
    @Bean
    // 标识
    @Description("获取指定城市的天气信息")
    public Function<WeatherFunction.WeatherRequest, String> weatherFunction() {
        return new WeatherFunction();
    }
}
告诉大模型请求可以调函数
package com.fox.toolcallingdemo.controller;


import com.fox.toolcallingdemo.tool.weather.method.WeatherTool;
import com.fox.toolcallingdemo.tool.weather.method.WeatherToolImpl;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/weather")
public class WeatherController {

    private final ChatClient dashScopeChatClient;

    /**
    * 默认全局调函数
    */
    public WeatherController(ChatClient.Builder chatClientBuilder) {
        this.dashScopeChatClient = chatClientBuilder
                .defaultFunctions("weatherFunction")
                //.defaultTools(new WeatherToolImpl())
                .build();
    }
-------------------------------------------------------------------------------------
    /**
     * 无工具版
     */
    @GetMapping("/chat")
    public String simpleChat(@RequestParam(value = "query", defaultValue = "北京今天的天气") String query) {
        return dashScopeChatClient.prompt(query).call().content();
    }
-------------------------------------------------------------------------------------
    /**
     * 调用工具版 - function
     */
    @GetMapping("/chat-tool-function")
    public String chatTranslateFunction(@RequestParam(value = "query", defaultValue = "北京今天的天气") String query) {

    // 就可以去找刚刚函数构建的bean
        return dashScopeChatClient.prompt(query).functions("weatherFunction").call().content();
    }

-------------------------------------------------------------------------------------

    /**
     * 调用工具版 - method
     */
    @GetMapping("/chat-tool-method")
    public String chatTranslateMethod(@RequestParam(value = "query", defaultValue = "北京今天的天气") String query) {

        return dashScopeChatClient.prompt(query).tools(new WeatherToolImpl()).call().content();
    }
}

GET:http://localhost:8088/weather/chat-tool-method
query  深圳今天的天气怎么样?
承接上面最后一个Method版
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\toolcalling-demo\src\main\java\com\fox\toolcallingdemo\tool\weather\method\WeatherTool.java
package com.fox.toolcallingdemo.tool.weather.method;

public interface WeatherTool {
    String getWeather(String city);
}
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\toolcalling-demo\src\main\java\com\fox\toolcallingdemo\tool\weather\method\WeatherToolImpl.java
package com.fox.toolcallingdemo.tool.weather.method;


import org.springframework.ai.tool.annotation.Tool;

public class WeatherToolImpl implements WeatherTool {
    @Override
    @Tool(description = "获取指定城市的天气信息。")
    public String getWeather(String city) {
        return "The weather in " + city + " is sunny.";
    }
}

MCP协议

模型上下文协议(即 Model Context Protocol,MCP)是一个开放协议,它规范了应用程序如何向大型语言模型(LLM)提供上下文。MCP 提供了一种统一的方式将 AI 模型连接到不同的数据源和工具,它定义了统一的集成方式。

MCP图+与function_calling区别

百炼智能体应用接入高德地图MCP服务

大模型服务平台百炼控制台
在智能体应用的技能添加MCP服务 Amap Maps需要提前开通即可

MCP开发实战

模型上下文协议(Model Context Protocol)
MCP 简介

模型上下文协议(即 Model Context Protocol,MCP)是一个开放协议,它规范了应用程序如何向大型语言模型(LLM)提供上下文。MCP 提供了一种统一的方式将 AI 模型连接到不同的数据源和工具,它定义了统一的集成方式。在开发智能体(Agent)的过程中,我们经常需要将将智能体与数据和工具集成,MCP 以标准的方式规范了智能体与数据及工具的集成方式,可以帮助您在 LLM 之上构建智能体(Agent)和复杂的工作流。目前已经有大量的服务接入并提供了 MCP server 实现,当前这个生态正在以非常快的速度不断的丰富中,具体可参见:MCP Servers

Spring AI MCP

Spring AI MCP 为模型上下文协议提供 Java 和 Spring 框架集成。它使 Spring AI 应用程序能够通过标准化的接口与不同的数据源和工具进行交互,支持同步和异步通信模式。

Spring AI MCP 采用模块化架构,包括以下组件:

  • Spring AI 应用程序:使用 Spring AI 框架构建想要通过 MCP 访问数据的生成式 AI 应用程序
  • Spring MCP 客户端:MCP 协议的 Spring AI 实现,与服务器保持 1:1 连接
  • MCP 服务器:轻量级程序,每个程序都通过标准化的模型上下文协议公开特定的功能
  • 本地数据源:MCP 服务器可以安全访问的计算机文件、数据库和服务
  • 远程服务:MCP 服务器可以通过互联网(例如,通过 API)连接到的外部系统

如何使用

要启用此功能,请将以下依赖项添加到您项目的 Mavenpom.xml文件中:

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-ai-mcp</artifactId>
    <version>1.0.0</version>
</dependency>

或者添加到您的 Gradlebuild.gradle文件中:

dependencies {
    implementation 'org.springframework.experimental:spring-ai-mcp:1.0.0'
}

Spring AI MCP 目前并没有在 Maven Central Repository 中提供。需要将 Spring milestone仓库添加到pom.xml中,才可以访问 Spring AI MCP 工件:

<repositories>
  <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/libs-milestone-local</url>
    <snapshots>
      <enabled>false</enabled>
    </snapshots>
  </repository>
</repositories>

要使用 MCP,首先需要创建McpClient,它提供了与 MCP server 的同步和异步通信能力。现在我们创建一个 McpClient 来注册 MCP Brave 服务和 ChatClient,从而让 LLM 调用它们:

var stdioParams = ServerParameters.builder("npx")
        .args("-y", "@modelcontextprotocol/server-brave-search")
        .addEnvVar("BRAVE_API_KEY", System.getenv("BRAVE_API_KEY"))
        .build();

var mcpClient = McpClient.using(new StdioClientTransport(stdioParams)).sync();

var init = mcpClient.initialize();

var chatClient = chatClientBuilder
        .defaultFunctions(mcpClient.listTools(null)
                .tools()
                .stream()
                .map(tool -> new McpFunctionCallback(mcpClient, tool))
                .toArray(McpFunctionCallback[]::new))
        .build();

String response = chatClient
        .prompt("Does Spring AI supports the Model Context Protocol? Please provide some references.")
        .call().content();

在上述代码中,首先通过npx命令启动一个独立的进程,运行@modelcontextprotocol/server-brave-search服务,并指定 Brave API 密钥。然后创建一个基于 stdio 的传输层,与 MCP server 进行通信。最后初始化与 MCP 服务器的连接。

要使用 McpClient,需要将McpClient注入到 Spring AI 的ChatClient中,从而让 LLM 调用 MCP server。在 Spring AI 中,可以通过 Function Callbacks 的方式将 MCP 工具转换为 Spring AI 的 Function,从而让 LLM 调用。

最后,通过ChatClient与 LLM 进行交互,并使用McpClient与 MCP server 进行通信,获取最终的返回结果。

官方的alibaba服务教学C:\Users\Pluminary\Desktop\HouDuan\spring-ai-alibaba-examples

Stdio传输层

Stdio(标准输入输出) 传输层是MCP最基本的传输实现方式。它通过进程间通信(IPC)实现

SSE传输层

SSE(Server-Sent Events) 传输层是基于HTTP的单向通信机制,专门用于服务器向客户端推送数据

MCP是什么?和Function calling有什么区别?美团面试题

MCP可以通过Client指定SSE 再去找一个远程地址直接就可以调用MCP就像拓展坞,MCPServer可以部署到云端给外部使用
Function Calling则需要自己手动去写一个去调用外部

这道面试题问的是:

MCP 是什么?它和 Function Calling 有什么区别?

来自美团,很可能是在考察你对 大模型应用框架(尤其是 RAG、工具调用、Agent 框架等)底层机制的理解,特别是近几年阿里推出的 Spring AI Alibaba MCP 模型调用框架。


✅ 一、什么是 MCP?

MCP(Model Call Protocol) 是阿里巴巴在 Spring AI Alibaba 框架中提出的一个模型调用协议,全称是:

Model Call Protocol(模型调用协议)

它的作用是:
🔧 统一模型接入规范,为不同的大模型(阿里通义、百度文心、ChatGLM、文生图、图生图等)提供一套通用、标准、结构化的调用协议和扩展机制

✅ MCP 解决的问题:

  • 🌀 不同大模型调用方式、参数格式五花八门 → MCP 提供统一接口抽象
  • 🛠 支持插件工具调用(Tool Calling)
  • 🔁 支持多轮对话记忆
  • 🤖 支持 Agent 架构(例如 RAG、Function调用等)
  • 📦 可以灵活切换底层大模型,实现模型无感知迁移(例如将通义大模型无缝替换为文心一言)

✅ MCP 的核心组件:

  1. MCPModel:统一的模型接口(封装了 prompt 处理、调用逻辑等)
  2. MCPFunctionCallingModel:支持 Function Calling 的模型
  3. MCPMessageMCPResponse:统一的输入/输出数据结构
  4. ToolDefinition, FunctionCall, ToolResult:对工具调用结构化建模

✅ 二、什么是 Function Calling?

Function Calling 是由 OpenAI 提出的技术,后被各大模型厂商采纳(如通义千问、百度文心也支持),它让大模型在对话中能够调用外部函数。

🔧 它的核心概念:

  • 大模型通过识别语义内容,决定是否调用一个函数(例如查天气、查数据库、生成图片等)
  • 由用户预定义函数列表(Function Schema),模型根据意图选择调用
  • 返回结构化 JSON 给开发者,由程序调用实际函数 → 执行后返回结果给模型

✅ Function Calling 实现的能力:

能力 说明
工具接入能力 让模型“用工具”去扩展知识或能力,如查资料、查库存、操作系统等
多轮调用链 支持调用链组合,如 A→B→C 的流程式工具使用
Agent能力基础 是 Agent 能够链式思维和计划任务执行的基础

✅ 三、MCP 和 Function Calling 的关系与区别

对比项 MCP Function Calling
本质 一种大模型调用协议标准 一种大模型的工具调用机制
是否支持 Function ✅ 支持,MCP 中内置了 Function Calling 抽象 ✅ 只关注“函数调用”,不管模型接入细节
作用范围 📦 模型统一封装、RAG、Chat、工具调用、记忆、代理等(整个调用框架) 🧠 模型如何调用函数、如何组织工具返回
抽象程度 高度抽象框架层,统一输入输出模型、适配各类模型 相对底层,是大模型的功能点
使用者 开发框架作者(如 Spring AI Alibaba) 模型厂商、应用开发者
代表项目 Spring AI Alibaba MCP、LangChain4j MCP、百度文心 RAGFlow 等 OpenAI GPT、通义千问、ChatGLM、Claude 等支持 Function Calling 的模型

✅ 四、通俗比喻理解

MCP 是一个大楼的 “总控中心”,可以调配各种大模型、工具、记忆、插件等资源

Function Calling 是这个大楼里的一套 “智能工具调度系统”,专门处理“这个请求要不要调用某个工具来完成”。


✅ 五、实际应用举例(代码层)

在 Spring AI Alibaba 中,你可以这样使用 MCP + FunctionCalling:

@Autowired
private FunctionCallingChatClient chatClient;

@PostMapping("/chat")
public String chat(@RequestBody String prompt) {
    Message response = chatClient.call(
        Message.of(prompt),
        FunctionCallingOptions.builder()
            .function(ToolDefinitions.fromClass(MyFunction.class))
            .build()
    );
    return response.getContent();
}

上面这段代码:

  • 使用 MCP 提供的 FunctionCallingChatClient 调用大模型
  • 绑定了一个 Function(MyFunction),模型根据上下文会自动决定是否调用它

✅ 总结答法(面试简明说法):

MCP 是阿里推出的模型调用协议,统一了模型接入、工具调用、Agent 调度等能力,是一种面向“统一模型开发框架”的抽象;Function Calling 是大模型的一种能力,允许它根据语义调用结构化函数。MCP 包含了 Function Calling 能力,但它作用更广,面向的是整套大模型调用生命周期,Function Calling 则聚焦在“如何让模型使用外部函数”这件事上。

电商智能客服项目

开发环境

JDK17、SpringBoot3.4.0、Spring AI 1.0.0-M6、Spring AI Alibaba 1.0.0-M6.1

项目背景

项目位于C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-ai
前端位于:C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-ai\frontend

可以右键通过终端打开文件夹
①:yarn install 安装依赖
②:yarn dev 启动项目

后端位于:C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-order【order.sql也在这里】;C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-ai分别启动两个模块的Application

使用MCP调用订单服务查询订单详情MCPServer是对订单层的包装

项目位于C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-order-mcp-clientC:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-order-mcp-server
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-order